package dkbta.ontology.loading;

import static dkbta.ontology.loading.OntologyParser.parseScaledSymbolicValues;
import static dkbta.util.Utils.enumForName;
import static dkbta.util.time.TimeUtils.convertTimeUnitToMillis;

import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import dkbta.ontology.Ontology;
import dkbta.ontology.elements.ElementType;
import dkbta.ontology.elements.TypedName;
import dkbta.ontology.elements.abstractions.TemporalSemanticProperty;
import dkbta.ontology.elements.abstractions.interpolation.PersistenceFunction;
import dkbta.ontology.elements.abstractions.interpolation.PersistenceTable;
import dkbta.ontology.loading.OntologyParser.ElementParser;
import dkbta.ontology.values.ScaledSymbolicValues;
import dkbta.ontology.values.SymbolicValues;
import dkbta.ontology.values.Values;
import dkbta.util.Utils;
import dkbta.util.time.TimeUnit;

abstract class AbstractionParser implements ElementParser{
	protected final Set<ElementType> _invalidAbstractionElements;

	protected final Ontology _ontology;

	public AbstractionParser(Ontology ontology,
		EnumSet<ElementType> invalidAnstractionElements){
		_ontology = ontology;
		_invalidAbstractionElements = invalidAnstractionElements;
	}

	@Override
	public void parseElement(Element abstraction, String name, String componentType)
			throws OntologyException{
		Set<TypedName> abstractedFrom = parseAbstractedFrom(abstraction, name);
		Set<String> necessaryContexts = parseNecessaryContexts(abstraction, name);
		Set<TemporalSemanticProperty> tsps = parseTemporalSemanticProperties(abstraction,
			name);
		ScaledSymbolicValues scaledValues = parseValues(abstraction);
		if (scaledValues == null){
			throw new OntologyException("State without symbolic values");
		}
		SymbolicValues values = scaledValues.getValues();

		PersistenceFunction persistFunc = parsePersistenceFunction(abstraction, values);
		if (persistFunc == null){
			throw new OntologyException("Abstraction without a persistence function");
		}
		parseAbstraction(abstraction, name, componentType, abstractedFrom,
			necessaryContexts, tsps, scaledValues, values, persistFunc);
	}

	protected abstract void parseAbstraction(Element abstraction, String name,
		String componentType, Set<TypedName> abstractedFrom,
		Set<String> necessaryContexts, Set<TemporalSemanticProperty> tsps,
		ScaledSymbolicValues scaledValues, SymbolicValues values,
		PersistenceFunction persistFunc) throws OntologyException;

	private Set<TypedName> parseAbstractedFrom(Element abstraction, String abstractionName)
			throws OntologyException{
		Element afsElement = OntologyParser.getXmlElement(abstraction, "AbstractedFrom");
		if (afsElement == null){
			throw new OntologyException("No abstracted-from element");
		}

		NodeList abstractedFromElements = afsElement.getChildNodes();
		int length = abstractedFromElements.getLength();
		Set<TypedName> afs = new HashSet<TypedName>();
		for (int i = 0; i < length; ++i){
			Node node = abstractedFromElements.item(i);
			if (node.getNodeType() != Node.ELEMENT_NODE){
				continue;
			}

			Element abstractedFrom = (Element)node;
			// Checking whether the type of element is valid for the type of the
			// abstraction
			ElementType et = enumForName(ElementType.class, abstractedFrom.getTagName());
			if (_invalidAbstractionElements.contains(et)){
				throw new OntologyException("Invalid abstracted-from element: " + et);
			}

			// Checking that a proper name was provided
			String elementName = abstractedFrom.getAttribute("name");
			if (Utils.isEmpty(elementName)){
				throw new OntologyException("Invalid abstracted-from name: "
						+ elementName);
			}

			// Checking that the same element doesn't appear twice on the list
			TypedName af = new TypedName(et, elementName);
			if (afs.contains(af)){
				throw new OntologyException(getElementType() + " " + abstractionName
						+ " has the abstracted-from " + af + " at least twice");
			}
			afs.add(af);
		}
		if (afs.isEmpty()){
			throw new OntologyException(
					"Abstractions must be have at least one abstracted-from element");
		}

		return afs;
	}

	private Set<String> parseNecessaryContexts(Element abstraction, String abstractionName)
			throws OntologyException{
		Element xml = OntologyParser.getXmlElement(abstraction, "NecessaryContexts");
		if (xml == null){
			return null;
		}

		NodeList contextNodes = xml.getElementsByTagName("Context");
		int length = contextNodes.getLength();
		Set<String> contexts = new HashSet<String>(length);
		for (int i = 0; i < length; ++i){
			Element contextElement = (Element)contextNodes.item(i);
			String name = contextElement.getAttribute("name");
			if (name.isEmpty()){
				throw new OntologyException(getElementType() + " " + abstractionName
						+ " has a necessary context without a name");
			}
			if (contexts.contains(name)){
				throw new OntologyException(getElementType() + " " + abstractionName
						+ " has the necessary context " + name + " at least twice");
			}
			contexts.add(name);
		}
		return contexts;
	}

	private Set<TemporalSemanticProperty> parseTemporalSemanticProperties(
		Element abstraction, String abstractionName) throws OntologyException{
		Element e = OntologyParser.getXmlElement(abstraction,
			"TemporalSemanticProperties");
		if (e == null){
			return null;
		}
		Set<TemporalSemanticProperty> tsProps = EnumSet
				.noneOf(TemporalSemanticProperty.class);

		NodeList tsps = e.getChildNodes();
		int length = tsps.getLength();
		for (int i = 0; i < length; ++i){
			Node node = tsps.item(i);
			if (node.getNodeType() != Node.ELEMENT_NODE){
				continue;
			}

			Element tspElement = (Element)node;
			String name = tspElement.getTagName();
			TemporalSemanticProperty tsp = enumForName(TemporalSemanticProperty.class,
				name);
			String value = tspElement.getAttribute("value");
			boolean val = Boolean.parseBoolean(value);

			// Missing properties are assumed to be false
			if (val){
				if (tsProps.contains(tsp)){
					throw new OntologyException("The temporal sematic property " + tsp
							+ " can only be set once");
				}else{
					tsProps.add(tsp);
				}
			}
		}
		return tsProps;
	}

	private ScaledSymbolicValues parseValues(Element abstraction)
			throws OntologyException{
		Element valuesElement = null;
		NodeList childNodes = abstraction.getChildNodes();
		int length = childNodes.getLength();
		for (int i = 0; i < length; ++i){
			Node node = childNodes.item(i);
			if (node.getNodeType() != Node.ELEMENT_NODE
					|| !node.getNodeName().equalsIgnoreCase("SymbolicValues")){
				continue;
			}
			if (valuesElement == null){
				valuesElement = (Element)node;
			}else{
				throw new OntologyException("Multile 'SymbolicValues' elements");
			}
		}

		return parseScaledSymbolicValues(valuesElement, false);
	}

	private PersistenceFunction parsePersistenceFunction(Element abstraction,
		Values values) throws OntologyException{
		Element persistFuncElement = OntologyParser.getXmlElement(abstraction,
			"PersistenceFunction");
		if (persistFuncElement == null){
			return null;
		}

		// Obtaining the number of entries
		NodeList valueNodes = persistFuncElement.getElementsByTagName("Value");
		Map<Object, PersistenceTable> pTables = new LinkedHashMap<Object, PersistenceTable>();

		for (int i = 0; i < valueNodes.getLength(); ++i){
			Element valueElement = (Element)valueNodes.item(i);
			// Parsing the return/abstracted value for the entry
			String rawValue = valueElement.getAttribute("name");
			Object parsedValue = values.parseAndValidate(rawValue);
			if (parsedValue == null){
				throw new OntologyException("Invalid/missing value '" + rawValue
						+ "' in the persistence function");
			}
			if (pTables.containsKey(parsedValue)){
				throw new OntologyException("Duplicate value '" + parsedValue
						+ "' in the persistence function");
			}

			// Parsing the persistence table
			PersistenceTable pt = parsePersistenceTable(valueElement, parsedValue);
			if (pt == null){
				throw new OntologyException("The value " + parsedValue
						+ " has no persistence table");
			}
			pTables.put(parsedValue, pt);
		}
		return new PersistenceFunction(pTables);
	}

	private PersistenceTable parsePersistenceTable(Element valueElement, Object value)
			throws OntologyException{
		Element pTableElement = OntologyParser.getXmlElement(valueElement,
			"PersistenceTable");
		if (pTableElement == null){
			return null;
		}

		TimeUnit timeUnit = Utils.enumForName(TimeUnit.class, pTableElement
				.getAttribute("timeUnit"));
		String type = pTableElement.getAttribute("type");
		NodeList gapNodes = pTableElement.getElementsByTagName("Gaps");

		int length = gapNodes.getLength();
		long[][] table = new long[length][length];

		for (int i = 0; i < length; ++i){
			Element gapElement = (Element)gapNodes.item(i);
			Node gapsTextNode = gapElement.getFirstChild();
			if (gapsTextNode == null || gapsTextNode.getNodeType() != Node.TEXT_NODE){
				throw new OntologyException("Malformed persistence table for value '"
						+ value + "' at row " + i);
			}
			String[] tokens = gapsTextNode.getTextContent().trim().split(" +");
			if (tokens.length != length){
				throw new OntologyException("The persistence table for value '" + value
						+ "' isn't square");
			}
			long[] gaps = new long[tokens.length];
			table[i] = gaps;
			try{
				for (int j = 0; j < tokens.length; ++j){
					int gap = Integer.parseInt(tokens[j]);
					if (gap < 0){
						throw new OntologyException("The persistence table for value '"
								+ value + "' at row " + i + " contains negative numbers");
					}
					gaps[j] = convertTimeUnitToMillis(timeUnit, gap);
				}
			}catch(NumberFormatException e){
				throw new OntologyException("The persistence table for value '" + value
						+ "' at row " + i + " contains non-numbers");
			}
		}

		return new PersistenceTable(timeUnit, type, table);
	}
	
}